package mapspinner;

import java.nio.FloatBuffer;
import java.nio.IntBuffer;

import mapspinner.persistence.datatypes.TerrainSettings;
import mapspinner.util.LODUtil;
import mapspinner.util.TerrainRenderPass;
import mapspinner.util.TerrainTexture;
import mapspinner.util.TerrainTexture4Pass;
import mapspinner.util.TextureRegistry;

import com.jme.math.FastMath;
import com.jme.math.Vector3f;
import com.jme.renderer.Renderer;
import com.jme.scene.Node;
import com.jme.scene.TexCoords;
import com.jme.scene.TriMesh;
import com.jme.scene.VBOInfo;
import com.jme.scene.state.TextureState;
import com.jme.system.DisplaySystem;
import com.jme.util.geom.BufferUtils;

public class TerrainNode extends Node {
	/**
	 * A terrain node defines a area of terrain that can be streamed, it
	 * includes methods for stitching itself to adjacent blocks to maintain the
	 * continuity of both normals and compressed height maps. It can also manage
	 * its own state as per creation of itself, building itself, stitching and
	 * lastly updating its own buffers.
	 * 
	 * The vertex buffer, texture coordinate buffer and index buffer is shared
	 * among all terrain nodes, all the terrain information is stored in the
	 * colour buffer and is set out as follows:
	 * 
	 * (channel 1: mix texture 1 and 2) (channel 2: add texture 3) (channel 3:
	 * add texture 4) (channel 4: height)
	 */

	private static final long serialVersionUID = 1L;
	public static final byte CREATED = 0;
	public static final byte BUILT = 1;
	public static final byte STITCHED = 2;
	public static final byte VBO_NORMALS_APPLIED = 3;
	public static final byte VBO_COLOR_APPLIED = 4;

	private static FloatBuffer VERTBUF;
	private static TexCoords TEXCOORDBUF;
	private static IntBuffer[] INDEXBUFS;
	private static int vboVertexID = -1;
	private static int vboTexCoordID = -1;

	private final TerrainSettings settings;

	private final TriMesh mesh = new TerrainMesh("TerrainMesh");
	private TerrainKey key;
	public boolean edited = false;
	private byte state;
	private String directory;
	private final TerrainBoundingBox boundingBox = new TerrainBoundingBox(this);
	private TerrainTexture terrainTexture;

	/**
	 * creates an instance of a terrain block
	 *
	 */
	public TerrainNode(final TerrainSettings settings, TerrainRenderPass pass) {
		super("TerrainNode");
		this.settings = settings;
		if (VERTBUF == null || TEXCOORDBUF == null || INDEXBUFS == null)
			buildSharedBuffers();
		final VBOInfo vbo = new VBOInfo(true);
		vbo.setVBOIndexEnabled(false);
		vbo.setVBONormalEnabled(true);
		vbo.setVBOVertexEnabled(true);
		vbo.setVBOColorEnabled(true);
		vbo.setVBOTextureEnabled(true);
		this.terrainTexture = pass.createTerrainTexture(mesh, settings);

		mesh.setVBOInfo(vbo);
		mesh.setVertexBuffer(VERTBUF);
		
		mesh.setRenderState(terrainTexture.getTextureState());
		mesh.setLocalScale(new Vector3f(settings.getHorizontalScale(), settings
				.getVerticalScale(), settings.getHorizontalScale())
				.mult(settings.getResolution()));
		mesh.setTextureCoords(TEXCOORDBUF);
		attachChild(mesh);
		mesh.setModelBound(boundingBox);
		state = CREATED;
	}

	public TerrainTexture getTerrainTexture() {
		return terrainTexture;
	}

	/**
	 * Sets up the terrain object, this can be done many times after
	 * initialisation for re-use of loaded blocks.
	 * 
	 * @param heightMap
	 *            the new heightMap
	 * @param colorBuf
	 *            the new splat and height buffer
	 * @param textures
	 *            the set of textures that will get splatted
	 * @param key
	 *            the new positional key to use.
	 */
	public void build(final float[] heightMap, final FloatBuffer colorBuf,
			final String[] textures, final TerrainKey key,
			final Vector3f position) {
		setName("Terrain: " + key.toString());
		this.key = key;

		if (terrainTexture.getTextures() != null) {
			for (final String texture : terrainTexture.getTextures())
				if (texture != null)
					TextureRegistry.decrementTexture(texture);
			terrainTexture.setTextures(null);
			terrainTexture.getTextureState().clearTextures();
		}
		edited = false;
		mesh
				.setVertexCount(settings.getResolution()
						* settings.getResolution());
		setLocalTranslation(position);
		if (colorBuf == null || colorBuf.limit() == 0)
			mesh.getColorBuffer().clear();
		else
			mesh.setColorBuffer(colorBuf);
		terrainTexture.setTextures(textures);
		updateRenderState();
		update(heightMap);
		calcNormals();
		state = BUILT;
	}

	/**
	 * builds the shared buffers for all blocks
	 */
	private void buildSharedBuffers() {
		TEXCOORDBUF = new TexCoords(BufferUtils.createVector2Buffer(settings
				.getResolution()
				* settings.getResolution()));
		final FloatBuffer texs = TEXCOORDBUF.coords;
		texs.clear();
		texs.rewind();
		for (int z = 0; z < settings.getResolution(); z++)
			for (int x = 0; x < settings.getResolution(); x++) {
				texs.put((float) x / (settings.getResolution() - 1));
				texs.put((float) z / (settings.getResolution() - 1));
			}
		VERTBUF = BufferUtils.createVector3Buffer(settings.getResolution()
				* settings.getResolution());
		for (int z = 0; z < settings.getResolution(); z++)
			for (int x = 0; x < settings.getResolution(); x++) {
				final int pointIndex = (x + z * settings.getResolution()) * 3;
				VERTBUF.put(pointIndex, (float) x / settings.getResolution());
				VERTBUF.put(pointIndex + 2, (float) z
						/ settings.getResolution());
			}

		INDEXBUFS = LODUtil.generateIndicis(settings.getResolution(), settings
				.getDetailLevels());
	}

	/**
	 * builds the normals for this block.
	 */
	public void calcNormals() {
		final FloatBuffer colorBuf = mesh.getColorBuffer();
		FloatBuffer normBuf = mesh.getNormalBuffer();
		if (normBuf == null)
			normBuf = BufferUtils.createVector3Buffer(mesh.getVertexCount());
		else
			normBuf.rewind();
		final Vector3f oppositePoint = new Vector3f();
		final Vector3f adjacentPoint = new Vector3f();
		final Vector3f rootPoint = new Vector3f();
		final Vector3f tempNorm = new Vector3f();
		int adj = 0, opp = 0, normalIndex = 0;
		for (int row = 0; row < settings.getResolution() - 1; row++)
			for (int col = 0; col < settings.getResolution(); col++) {
				rootPoint.set(VERTBUF.get(normalIndex * 3), colorBuf
						.get(normalIndex * 4 + 3)
						* 1 / settings.getHorizontalScale(), VERTBUF
						.get(normalIndex * 3 + 2));
				if (col == settings.getResolution() - 1) { // last column
					// except
					// for last
					// row
					// left cross down
					adj = normalIndex - 1;
					opp = normalIndex + settings.getResolution();
				} else { // most cases
					// down cross right
					adj = normalIndex + settings.getResolution();
					opp = normalIndex + 1;
				}
				adjacentPoint.set(VERTBUF.get(adj * 3), colorBuf
						.get(adj * 4 + 3)
						* 1 / settings.getHorizontalScale(), VERTBUF
						.get(adj * 3 + 2));
				oppositePoint.set(VERTBUF.get(opp * 3), colorBuf
						.get(opp * 4 + 3)
						* 1 / settings.getHorizontalScale(), VERTBUF
						.get(opp * 3 + 2));
				tempNorm.set(adjacentPoint).subtractLocal(rootPoint)
						.crossLocal(oppositePoint.subtractLocal(rootPoint))
						.normalizeLocal();
				BufferUtils.setInBuffer(tempNorm, normBuf, normalIndex);
				normalIndex++;
			}
		mesh.setNormalBuffer(normBuf);
	}

	/**
	 * applies a single formal from another block to this one
	 * 
	 * @param fromBlock
	 *            the block to retrieve the normal from
	 * @param fromIndex
	 *            the index of the normal to retrieve
	 * @param toIndex
	 *            the index to apply it to locally
	 */
	private void copyNormal(final TerrainNode fromBlock, final int fromIndex,
			final int toIndex) {
		final FloatBuffer fromBuf = fromBlock.getMesh().getNormalBuffer();
		final FloatBuffer normBuf = mesh.getNormalBuffer();
		normBuf.put(toIndex * 3, fromBuf.get(fromIndex * 3));
		normBuf.put(toIndex * 3 + 1, fromBuf.get(fromIndex * 3 + 1));
		normBuf.put(toIndex * 3 + 2, fromBuf.get(fromIndex * 3 + 2));
	}

	@Override
	/*
	 * * draw this block, applying any unfinished buffer updates in the opengl
	 * thread if neccesary.
	 */
	public void draw(final Renderer r) {
		if (state == VBO_COLOR_APPLIED)
			super.draw(r);
	}

	public String getDirectory() {
		return directory;
	}

	/**
	 * <code>getHeight</code> returns the height of an arbitrary point on the If
	 * the point is between height point values, the height is linearly
	 * interpolated. This provides smooth height calculations. If the point
	 * provided is not within the bounds of the height map, the NaN float value
	 * is returned (Float.NaN).
	 * 
	 * @param x
	 *            the x coordinate to check.
	 * @param z
	 *            the z coordinate to check.
	 * @return the height at the provided location.
	 */
	public float getHeight(float x, float z) {
		x /= settings.getHorizontalScale();
		z /= settings.getHorizontalScale();
		final float col = FastMath.floor(x);
		final float row = FastMath.floor(z);

		if (col < 0 || row < 0 || col >= settings.getResolution() - 1
				|| row >= settings.getResolution() - 1)
			return Float.NaN;
		final float intOnX = x - col, intOnZ = z - row;

		float topLeft, topRight, bottomLeft, bottomRight;

		final int focalSpot = (int) (col + row * settings.getResolution());

		// find the heightmap point closest to this position (but will always
		// be to the left ( < x) and above (< z) of the spot.
		topLeft = getPoint(focalSpot) * mesh.getLocalScale().y;

		// now find the next point to the right of topLeft's position...
		topRight = getPoint(focalSpot + 1) * mesh.getLocalScale().y;

		// now find the next point below topLeft's position...
		bottomLeft = getPoint(focalSpot + settings.getResolution())
				* mesh.getLocalScale().y;

		// now find the next point below and to the right of topLeft's
		// position...
		bottomRight = getPoint(focalSpot + settings.getResolution() + 1)
				* mesh.getLocalScale().y;

		// Use linear interpolation to find the height.
		if (intOnX > intOnZ)
			return (1 - intOnX) * topLeft + (intOnX - intOnZ) * topRight
					+ intOnZ * bottomRight;
		else
			return (1 - intOnZ) * topLeft + (intOnZ - intOnX) * bottomLeft
					+ intOnX * bottomRight;
	}

	/**
	 * @return the height map of this block
	 */
	public float[] getHeightMap() {
		final FloatBuffer colorBuf = mesh.getColorBuffer();
		final float[] heightMap = new float[settings.getResolution()
				* settings.getResolution()];
		for (int x = 0; x < heightMap.length; x++)
			heightMap[x] = colorBuf.get(x * 4 + 3);
		return heightMap;
	}

	/**
	 * @return this terrains positional key
	 */
	public TerrainKey getKey() {
		return key;
	}

	/**
	 * @return the mesh object for the terrain
	 */
	public TriMesh getMesh() {
		return mesh;
	}

	/**
	 * gets a height at a certain index
	 * 
	 * @param index
	 *            the index of the height to get
	 * @return the height at that index
	 */
	public float getPoint(final int point) {
		return mesh.getColorBuffer().get(point * 4 + 3);
	}

	/**
	 * retrieves a height from this block
	 * 
	 * @param x
	 *            the x index
	 * @param z
	 *            the z index
	 * @return the height at that index
	 */
	public float getPoint(final int x, final int z) {
		return getPoint(x + z * settings.getResolution());
	}

	/**
	 * @return the state (build progress) of this block
	 */
	public byte getState() {
		return state;
	}

	public void setDirectory(final String directory) {
		this.directory = directory;
	}

	/**
	 * Sets wich index buffer to use from the static array of possible buffers
	 * 
	 * @param bufNum
	 *            the new index of the buffer from the array.
	 */
	public void setIndexBuffer(final int bufNum) {
		mesh.setIndexBuffer(INDEXBUFS[bufNum]);
	}

	/**
	 * sets a single point on this block
	 * 
	 * @param x
	 *            the x index
	 * @param z
	 *            the z index
	 * @param height
	 *            the new height
	 */
	public void setPoint(final int x, final int z, final float height) {
		mesh.getColorBuffer().put((x + z * settings.getResolution()) * 4 + 3,
				height);
	}

	/**
	 * sets the current state (building progress) of this block
	 * 
	 * @param state
	 *            the new state
	 */
	public void setState(final byte state) {
		this.state = state;
	}

	/**
	 * stitch the block only if its not already stitched if height stitching was
	 * successful, normals get stitched and autoTexturing gets applied.
	 * 
	 * @param terrainNodes
	 */
	public void stitch(final TerrainNode[] terrainNodes) {
		if (state != BUILT)
			return;
		if (!stitchHeights(terrainNodes))
			return;
		stitchNormals(terrainNodes);
		state = STITCHED;
	}

	/**
	 * ensure the heights of this block matches those of its adjacent blocks
	 * 
	 * @param adjacentTerrainNodes
	 *            an array of this block, and the relevant adjacent ones.
	 * @return true if successful
	 */
	public boolean stitchHeights(final TerrainNode[] adjacentTerrainNodes) {
		final FloatBuffer colorBuf = mesh.getColorBuffer();
		if (adjacentTerrainNodes == null)
			return false;

		int x = settings.getResolution() - 1, z = 0;
		for (; z < settings.getResolution() - 1; z++)
			colorBuf.put((x + z * settings.getResolution()) * 4 + 3,
					adjacentTerrainNodes[1].getMesh().getColorBuffer().get(
							z * settings.getResolution() * 4 + 3));
		z = settings.getResolution() - 1;
		for (x = 0; x < settings.getResolution() - 1; x++)
			colorBuf.put((x + z * settings.getResolution()) * 4 + 3,
					adjacentTerrainNodes[2].getMesh().getColorBuffer().get(
							x * 4 + 3));
		x = settings.getResolution() - 1;
		z = settings.getResolution() - 1;
		colorBuf.put((x + z * settings.getResolution()) * 4 + 3,
				adjacentTerrainNodes[3].getMesh().getColorBuffer().get(3));
		return true;
	}

	/**
	 * ensure the normals of this block matches those of its adjacent blocks
	 * 
	 * @param adjacentTerrainNodes
	 *            an array of this block, and the relevant adjacent ones.
	 */
	public void stitchNormals(final TerrainNode[] adjacentTerrainNodes) {
		int x = 0;
		int y = 0;
		if (adjacentTerrainNodes == null)
			return;
		for (; y < settings.getResolution() - 1; y++)
			copyNormal(adjacentTerrainNodes[1], y * settings.getResolution(),
					settings.getResolution() - 1 + y * settings.getResolution());
		for (; x < settings.getResolution(); x++)
			copyNormal(adjacentTerrainNodes[2], x, x + y
					* settings.getResolution());
		copyNormal(adjacentTerrainNodes[3], 0, settings.getResolution() - 1
				+ (settings.getResolution() - 1) * settings.getResolution());
	}

	/**
	 * update the heights of this terrain block from a heightmap
	 * 
	 * @param heightMap
	 *            the heightmap to rebuild from
	 */
	public void update(final float[] heightMap) {
		for (int x = 0; x < heightMap.length; x++)
			mesh.getColorBuffer().put(x * 4 + 3, heightMap[x]);
	}

	public boolean update(final Renderer r) {
		if (state == VBO_NORMALS_APPLIED - 1) {
			final VBOInfo vbo = mesh.getVBOInfo();
			if (vboVertexID == -1)
				vboVertexID = vbo.getVBOVertexID();
			else if (vboVertexID != vbo.getVBOVertexID())
				vbo.setVBOVertexID(vboVertexID);
			if (vboTexCoordID == -1)
				vboTexCoordID = vbo.getVBOTextureID(0);
			else if (vboTexCoordID != vbo.getVBOTextureID(0))
				vbo.getVBOTextureID(0);

			mesh.updateModelBound();

			final FloatBuffer normBuf = mesh.getNormalBuffer();
			mesh.getVBOInfo().setVBONormalID(-1);
			r.deleteVBO(normBuf);
			++state;
			return true;
		}
		if (state == VBO_COLOR_APPLIED - 1) {
			final FloatBuffer colorBuf = mesh.getColorBuffer();
			mesh.getVBOInfo().setVBOColorID(-1);
			r.deleteVBO(colorBuf);
			++state;
			return true;
		}
		return false;
	}
}
